﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

//using System.Windows.Forms;
namespace lox_z12Class
{
    class Interpreter : Expr.Visitor<Object>, Stmt.Visitor<string>//Void
    {
        public string result = "";
        Environment globals = new Environment();
        private Environment environment = null;//globals
        private Dictionary<Expr, int> locals = new Dictionary<Expr, int>();
        public Interpreter()
        {
            environment = globals;
            //    globals.define("clock", new LoxCallable() 
            //    {
            //    override public int arity() { return 0; }

            //    override public Object call(Interpreter interpreter, List<Object> arguments)
            //    {
            //        return (double) System.currentTimeMillis() / 1000.0;
            //    }

            //    override public String toString() { return "<native fn>"; }
            //});
        }

        public void interpret(List<Stmt> statements)
        { // [void]
            try
            {
                foreach (Stmt statement in statements)
                {
                    execute(statement);
                }
            }
            catch (RuntimeError error)
            {
                //Lox.runtimeError(error);
                //MessageBox.Show("运行时错误：" + error.Message);
                result += "\r\n运行时错误：" + error.Message;
            }
        }
        private Object evaluate(Expr expr) { return expr.accept(this); }
        private void execute(Stmt stmt) { stmt.accept(this); }
        public void resolve(Expr expr, int depth) { locals[expr]=depth; }
        public void executeBlock(List<Stmt> statements, Environment environment)
        {
            Environment previous = this.environment;
            try
            {
                this.environment = environment;

                foreach (Stmt statement in statements)
                {
                    execute(statement);
                }
            }
            finally
            {
                this.environment = previous;
            }
        }
        public string visitBlockStmt(Stmt.Block stmt)
        {
            executeBlock(stmt.statements, new Environment(environment));
            return null;
        }
        public string visitClassStmt(Stmt.Class stmt)
        {
            environment.define(stmt.name.lexeme, null);

            Dictionary<String, LoxFunction> methods = new Dictionary<String, LoxFunction>();
            foreach (Stmt.Function method in stmt.methods) {
                bool isOk=method.name.lexeme=="init";
                LoxFunction function = new LoxFunction(method, environment, isOk);
                methods.Add(method.name.lexeme, function);
            }

            LoxClass klass = new LoxClass(stmt.name.lexeme, methods);
            environment.assign(stmt.name, klass);
            return null;
        }
        public string visitExpressionStmt(Stmt.Expression stmt)
        {
            evaluate(stmt.expression);
            return null;
        }
        public string visitFunctionStmt(Stmt.Function stmt)
        {
            LoxFunction function = new LoxFunction(stmt, environment, false);
            environment.define(stmt.name.lexeme, function);
            return null;
        }
        public string visitIfStmt(Stmt.If stmt)
        {
            if (isTruthy(evaluate(stmt.condition)))
            {
                execute(stmt.thenBranch);
            }
            else if (stmt.elseBranch != null)
            {
                execute(stmt.elseBranch);
            }
            return null;
        }
        public string visitPrintStmt(Stmt.Print stmt)
        {
            Object value = evaluate(stmt.expression);
            //System.out.println(stringify(value));
            //MessageBox.Show(stringify(value));
            result += "\r\n" + stringify(value);
            return null;
        }
        public string visitReturnStmt(Stmt.Return stmt)
        {
            Object value = null;
            if (stmt.value != null)
                value = evaluate(stmt.value);

            throw new Return(value);
        }
        public string visitVarStmt(Stmt.Var stmt)
        {
            Object value = null;
            if (stmt.initializer != null)
            {
                value = evaluate(stmt.initializer);
            }

            environment.define(stmt.name.lexeme, value);
            return null;
        }
        public string visitWhileStmt(Stmt.While stmt)
        {
            while (isTruthy(evaluate(stmt.condition)))
            {
                execute(stmt.body);
            }
            return null;
        }
        public Object visitAssignExpr(Expr.Assign expr)
        {
            Object value = evaluate(expr.value);
            int distance = 0;
            if (locals.ContainsKey(expr))
            {
                distance = locals[expr];
                environment.assignAt(distance, expr.name, value);
            }
            else
            {
                globals.assign(expr.name, value);
            }


            return value;
        }
        public Object visitBinaryExpr(Expr.Binary expr)
        {
            Object left = evaluate(expr.left);
            Object right = evaluate(expr.right); // [left]

            switch (expr._operator.type)
            {
                case TokenType.BANG_EQUAL:
                    return !isEqual(left, right);
                case TokenType.EQUAL_EQUAL:
                    return isEqual(left, right);
                case TokenType.GREATER:
                    checkNumberOperands(expr._operator, left, right);
                    return (double)left > (double)right;
                case TokenType.GREATER_EQUAL:
                    checkNumberOperands(expr._operator, left, right);
                    return (double)left >= (double)right;
                case TokenType.LESS:
                    checkNumberOperands(expr._operator, left, right);
                    return (double)left < (double)right;
                case TokenType.LESS_EQUAL:
                    checkNumberOperands(expr._operator, left, right);
                    return (double)left <= (double)right;
                case TokenType.MINUS:
                    checkNumberOperands(expr._operator, left, right);
                    return (double)left - (double)right;
                case TokenType.PLUS:
                    if (left is Double && right is Double)
                    {
                        return (double)left + (double)right;
                    } // [plus]

                    if (left is string && right is string)
                    {
                        return (String)left + (String)right;
                    }

                    throw new RuntimeError(expr._operator, "Operands must be two numbers or two strings.");
                case TokenType.SLASH:
                    checkNumberOperands(expr._operator, left, right);
                    return (double)left / (double)right;
                case TokenType.STAR:
                    checkNumberOperands(expr._operator, left, right);
                    return (double)left * (double)right;
            }

            // Unreachable.
            return null;
        }
        public Object visitCallExpr(Expr.Call expr)
        {
            Object callee = evaluate(expr.callee);

            List<Object> arguments = new List<Object>();
            foreach (Expr argument in expr.arguments)
            { // [in-order]
                arguments.Add(evaluate(argument));
            }

            if (!(callee is LoxCallable))
            {
                throw new RuntimeError(expr.paren, "Can only call functions and classes.");
            }

            LoxCallable function = (LoxCallable)callee;
            if (arguments.Count() != function.arity())
            {
                throw new RuntimeError(expr.paren,
                                       "Expected " + function.arity() + " arguments but got " + arguments.Count() + ".");
            }

            return function.call(this, arguments);
        }
         public Object visitGetExpr(Expr.Get expr)
        {
            Object obj = evaluate(expr._object);
            if (obj is LoxInstance) {
                return ((LoxInstance)obj).get(expr.name);
            }

            throw new RuntimeError(expr.name, "Only instances have properties.");
        }
        public Object visitGroupingExpr(Expr.Grouping expr) { return evaluate(expr.expression); }
        public Object visitLiteralExpr(Expr.Literal expr) { return expr.value; }
        public Object visitLogicalExpr(Expr.Logical expr)
        {
            Object left = evaluate(expr.left);

            if (expr._operator.type == TokenType.OR)
            {
                if (isTruthy(left))
                    return left;
            }
            else
            {
                if (!isTruthy(left))
                    return left;
            }

            return evaluate(expr.right);
        }
        public Object visitSetExpr(Expr.Set expr)
        {
            Object obj = evaluate(expr._object);

            if (!(obj is LoxInstance)) { // [order]
                throw new RuntimeError(expr.name, "Only instances have fields.");
            }

            Object value = evaluate(expr.value);
            ((LoxInstance)obj).set(expr.name, value);
            return value;
        }
        public Object visitThisExpr(Expr.This expr) { return lookUpVariable(expr.keyword, expr); }
        public Object visitUnaryExpr(Expr.Unary expr)
        {
            Object right = evaluate(expr.right);

            switch (expr._operator.type)
            {
                case TokenType.BANG:
                    return !isTruthy(right);
                case TokenType.MINUS:
                    checkNumberOperand(expr._operator, right);
                    return -(double)right;
            }

            // Unreachable.
            return null;
        }
        //public Object visitVariableExpr(Expr.Variable expr) { return environment.get(expr.name); }
        public Object visitVariableExpr(Expr.Variable expr) { return lookUpVariable(expr.name, expr); }
        private Object lookUpVariable(Token name, Expr expr)
        {

            int distance =0;
            if(locals.ContainsKey(expr))
            {
                distance=locals[expr];
                return environment.getAt(distance, name.lexeme);
            } 
            else 
            {
                return globals.get(name);
            }
        }

        private void checkNumberOperand(Token pOperator, Object operand)
        {
            if (operand is Double)
                return;
            throw new RuntimeError(pOperator, "Operand must be a number.");
        }
        private void checkNumberOperands(Token pOperator, Object left, Object right)
        {
            if (left is Double && right is Double)
                return;
            // [operand]
            throw new RuntimeError(pOperator, "Operands must be numbers.");
        }
        private bool isTruthy(Object obj)
        {
            if (obj == null)
                return false;
            if (obj is Boolean)
                return (bool)obj;
            return true;
        }
        private bool isEqual(Object a, Object b)
        {
            if (a == null && b == null)
                return true;
            if (a == null)
                return false;

            return a == b;
        }
        private string stringify(Object obj)
        {
            if (obj == null)
                return "nil";

            if (obj is Double)
            {
                String text = obj.ToString();
                //if (text.endsWith(".0")) {
                //    text = text.Substring(0, text.Length - 2);
                //}
                return text;
            }

            return obj.ToString();
        }
    }
}
